"""Define a general interface for simulators which solve Maxwell's equations.

EMopt contains multiple different types of 2D and 3D solvers. In order to make
their usage consistent and also to make them all as compatible as possible with
the adjoint method/sensitivity framework, we define a general interface that
all Maxwell solvers must implement. This allows us to standard functionality
and ensure compatibility between modules.
"""

from builtins import object
from abc import ABCMeta, abstractmethod
from future.utils import with_metaclass

__author__ = "Andrew Michaels"
__license__ = "GPL License, Version 3.0"
__version__ = "2019.5.6"
__maintainer__ = "Andrew Michaels"
__status__ = "development"

class MaxwellSolver(with_metaclass(ABCMeta, object)):
    """An interface for defining a solver for Maxwell's Equations.

    Methods
    -------
    solve_forward(self)
        Simulate Maxwell's equations
    solve_adjoint(self)
        Simulate the transposed Maxwell's equations
    get_field(self, component)
        Get the desired component of the raw (uninterpolated) field
    get_field_interp(self, component)
        Get the desired component of the interpolated field
    get_adjoint_field(self, component)
        Get the desired component of the raw (uninterpolated) adjoint field
    build(self)
        Build the system matrix :math:`A`.
    update(self)
        Update the system matrix :math:`A`
    set_sources(self, src)
        Set the discretized current sources used in the forward simulation.
    set_adjoint_sources(self, src)
        Set the discretized current sources used in the adjoint simulation.
    get_source_power(self)
        Get the total power generated by the sources.
    get_A_diag(self, vdiag=None)
        Retrieve the diagonal elements of :math:`A`

    Attributes
    ----------
    field_domains : list of DomainCoordinates
        The list of DomainCoordinates in which fields are recorded immediately
        following a forward solve.
    saved_fields : list of tuples
        The list of fields saved in in the stored DomainCoordinates
    source_power : float
        The source power injected into the system.
    ndims : int
        number of cartesian dimensions in simulation (2 = 2D sim, 3 = 3D sim)
    """

    def __init__(self, ndims):
        self._field_domains = []
        self._saved_fields = []
        self._source_power = 0.0
        self._ndims = ndims

    @property
    def field_domains(self):
        return self._field_domains

    @field_domains.setter
    def field_domains(self, domains):
        self._field_domains = domains

    @property
    def saved_fields(self):
        return self._saved_fields

    @property
    def source_power(self):
        return self._source_power

    @property
    def ndims(self):
        return self._ndims

    @abstractmethod
    def solve_forward(self):
        pass

    @abstractmethod
    def solve_adjoint(self):
        pass

    @abstractmethod
    def get_field(self, component, domain=None):
        pass

    @abstractmethod
    def get_field_interp(self, component, domain=None):
        pass

    @abstractmethod
    def get_adjoint_field(self, component, domain=None):
        pass

    @abstractmethod
    def build(self):
        pass

    @abstractmethod
    def update(self, bbox=None):
        pass

    @abstractmethod
    def set_sources(self, src):
        pass

    @abstractmethod
    def set_adjoint_sources(self, src):
        pass

    @abstractmethod
    def update_saved_fields(self):
        pass

    @abstractmethod
    def get_source_power(self, src):
        """
        Notes
        -----
        This should exclude any influence due to non-physical boundary
        conditions like PMLs (if possible)
        """
        pass

    @abstractmethod
    def get_A_diag(self):
        """Get the diagonal of the matrix which represents Maxwell's Equations.

        Typically, this contains the permittivity and permeability
        distribution. The form of diag[A] is unrestricted. The only requirement
        is that diag[A] be a COPY! i.e., it cannot share data with any internal
        matrices, arrays, etc.
        """
        pass

    @abstractmethod
    def calc_ydAx(self, Adiag0):
        """Calculate the product y^T*dA*x which is integral to the adjoint
        method.

        Given an adjoint simulation vector :math:`y` and a forward simulation
        vector :math:`x`, compute the product

        ..math:
            y^T (A_1 - A_0) x

        where :math:`A_0` is an 'initial' form of :math:`A` which is supplied.
        This assumes that an up-to-date version of :math:`A` is possessed by
        this object.
        """
        pass

    @property
    def X(self):
        raise NotImplemented('Simulations must implement the "X" property.')

    @property
    def Y(self):
        raise NotImplemented('Simulations must implement the "Y" property.')

    @property
    def Z(self):
        raise NotImplemented('3D simulations must implement the "Z" property.')
